package com.icw.utility;

import java.beans.PropertyDescriptor;
import java.io.Closeable;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.servlet.http.HttpServletResponse;

import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.util.FileUtil;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.BeanUtils;

import com.icw.basis.BaseGeneric;
import com.icw.basis.BasePoEntity;
import com.icw.basis.MyRunnable;
import com.icw.configuration.ConstSettings;
import com.icw.decoupling.defi.module.GenericSrv;
import com.icw.decoupling.defi.proxy.GenericSrvProxy;
import com.icw.decoupling.defi.proxy.RestfulApiProxy;
import com.icw.decoupling.defi.proxy.RestfulMgrProxy;
import com.icw.decoupling.defi.tool.DataExchange;
import com.icw.resource.inner.entity.IcwSysParam;
import com.icw.resource.inner.entity.IcwUser;

public abstract class IcwUtils {

	private static ThreadLocal<IcwUser> USER_THREAD_LOCAL = new ThreadLocal<>();

	public static void setUser(IcwUser user) {
		USER_THREAD_LOCAL.set(user);
	}

	public static void setUser(IcwUser user, MyRunnable run) throws Exception {
		IcwUser oriUser = IcwUtils.getUser();
		try {
			IcwUtils.setUser(user);
			run.run();
		} finally {
			IcwUtils.setUser(oriUser);
		}
	}

	public static <T> T setUser(IcwUser user, Callable<T> run) throws Exception {
		IcwUser oriUser = IcwUtils.getUser();
		try {
			IcwUtils.setUser(user);
			return run.call();
		} finally {
			IcwUtils.setUser(oriUser);
		}
	}

	public static IcwUser getUser() {
		return USER_THREAD_LOCAL.get();
	}

	@SafeVarargs
	public static <A> A ifNull(A ori, If<A>... objs) {
		if (ori != null) {
			return ori;
		} else {
			for (If<A> temp : objs) {
				A t = temp.exec();
				if (t != null) {
					return t;
				}
			}
			return null;
		}
	}

	public static <A> Map<String, A> list2map(Collection<A> list, KeyOf<A> keyFunc) {
		Map<String, A> map = new LinkedHashMap<String, A>();
		for (A item : list) {
			map.put(keyFunc.exec(item), item);
		}
		return map;
	}

	public static <A> Map<String, A> list2map(A[] list, KeyOf<A> keyFunc) {
		return list2map(Arrays.asList(list), keyFunc);
	}

	public static interface KeyOf<A> {

		String exec(A item);
	}

	public static interface If<A> {

		A exec();
	}

	public static String cutHost(String referer) {
		Pattern p = Pattern.compile("(http|https)(:/{2})(([^\\/])+)");
		Matcher matcher = p.matcher(referer);
		matcher.find();
		return matcher.group();
	}

	public static boolean isRoleOf(String roleName) {
		return IcwUtils.getUser() != null && IcwUtils.getUser().getRoles() != null && IcwUtils.getUser().getRoles().stream().anyMatch(r -> roleName.equals(r.getName()));
	}

	public static boolean isAdmin() {
		return isRoleOf(ConstSettings.NAME_OF_ADMIN_ROLE) || (IcwUtils.getUser() != null && IcwUtils.getUser().getUsername().equals(ConstSettings.DEFAULT_USERNAME));
	};

	public static Map<String, Object> idObj(Object o) {
		Object id;
		if (o == null) {
			id = null;
		} else if (String.class.isInstance(o)) {
			id = o;
		} else {
			id = PoEntityHelper.readValueByProperty(o, "id");
		}
		if (id == null) {
			return null;
		} else {
			return IcwUtils.mapObject("id", id);
		}
	}

	public static void sleep(int i) {
		try {
			Thread.sleep(i);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public static <A> A sleep(int i, Callable<Boolean> boolCall, Callable<A> funcCall) {
		try {
			while (boolCall.call()) {
				IcwUtils.sleep(i);
			}
			return funcCall.call();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public static <A> A tryCatch(Callable<A> call) {
		try {
			return call.call();
		} catch (Exception e) {
			e.printStackTrace();
			return (A) null;
		}
	}

	public static void tryCatch(MyRunnable run) {
		tryCatch(() -> {
			run.run();
			return null;
		});
	}

	public static <A, V> A tryCatch(Invoke<A, V> inv, V arg) {
		try {
			return inv.exec(arg);
		} catch (Exception e) {
			return (A) null;
		}
	}

	public static interface Invoke<A, V> {

		A exec(V v) throws Exception;
	}

	public static void newThread(Runnable run) {
		new Thread(run).start();
	}

	public static void closeClosable(Closeable... closables) {
		Stream.of(closables).forEach(c -> {
			if (c != null) {
				try {
					c.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	public static void copyStreamAndClose(InputStream in, OutputStream out) throws Exception {
		try {
			FileUtil.copyStream(in, out);
		} finally {
			closeClosable(in, out);
		}
	}

	public static String nextSeq(String seqKey) throws Exception {
		return BeanCtxUtils.getBeanByType(GenericSrvProxy.class).exec(() -> {
			@SuppressWarnings("unchecked")
			GenericSrv<IcwSysParam> sps = BaseGeneric.beanOf(GenericSrv.class, IcwSysParam.class);
			IcwSysParam seq = sps.findByName(seqKey + ",0");
			seq.setValue(Integer.parseInt(seq.getValue()) + 1 + "");
			sps.save(seq);
			return seq.getValue();
		});
	}

	@SuppressWarnings("unchecked")
	public static <T extends Map<String, Object>> T setValue(T obj, String path, String value) {
		String[] paths = path.split("\\.");
		Map<String, Object> finalObject = obj;
		for (int i = 0; i < paths.length - 1; i++) {
			Object tmepMap;
			if ((tmepMap = finalObject.get(paths[i])) == null) {
				finalObject.put(paths[i], tmepMap = new LinkedHashMap<String, Object>());
			}
			finalObject = (Map<String, Object>) tmepMap;
		}
		finalObject.put(paths[paths.length - 1], value);
		return obj;
	}

	public static interface Empty {

		void call() throws Exception;
	}

	public static interface NotEmpty<T> {

		T call() throws Exception;
	}

	public static <T> T tryFinlly(NotEmpty<T> tryCall, Empty finallyCall) throws Exception {
		try {
			return tryCall.call();
		} finally {
			finallyCall.call();
		}
	}

	private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	static {
		sdf.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
	}

	public static DateFormat dateFormat() {
		return sdf;
	}

	public static DateFormat dateFormat(String pattern) {
		SimpleDateFormat tsdf = new SimpleDateFormat(pattern);
		tsdf.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
		return tsdf;
	}

	public static Map<String, Object> mapObject(Object... kvs) {
		Map<String, Object> map = new LinkedHashMap<>();
		for (int i = 0; i < kvs.length; i = i + 2) {
			if (kvs[i] != null) {
				map.put((String) kvs[i], kvs[i + 1]);
			}
		}
		return map;
	}

	public static String encode(String text) {
		try {
			final Base64.Encoder encoder = Base64.getEncoder();
			final byte[] textByte = text.getBytes("UTF-8");
			return encoder.encodeToString(textByte);
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}

	public static String decode(String text) {
		try {
			final Base64.Decoder decoder = Base64.getDecoder();
			return new String(decoder.decode(text), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}

	public static String toUnderScoreCase(String name) {
		try {
			Class<?> clazz = Class.forName("org.springframework.jdbc.core.BeanPropertyRowMapper");
			Method method = clazz.getDeclaredMethod("underscoreName", String.class);
			method.setAccessible(true);
			return (String) method.invoke(clazz.newInstance(), name);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public static String toFirstLowerCase(String name) {
		return name.substring(0, 1).toLowerCase() + name.substring(1);
	}

	public static String toFirstUpperCase(String name) {
		return name.substring(0, 1).toUpperCase() + name.substring(1);
	}

	@SuppressWarnings("unchecked")
	public static Object getQueryListFirstFieldValue(List<?> list) {
		if (list.size() == 0) return null;
		else return ((Object[]) ((Map<String, Object>) list.get(0)).get("values"))[0];
	}

	@SuppressWarnings("unchecked")
	public static Object[] getQueryListFirstFieldValues(List<?> list) {
		return list.stream().map(l -> ((Object[]) ((Map<String, Object>) l).get("values"))[0]).filter(o -> o != null).toArray();
	}

	public static String getQueryListFirstFieldString(List<?> list) {
		return (String) getQueryListFirstFieldValue(list);
	}

	public static String[] getQueryListFirstFieldStrings(List<?> list) {
		return Stream.of(IcwUtils.getQueryListFirstFieldValues(list)).map(o -> String.class.cast(o)).collect(Collectors.toList()).toArray(ArrayUtils.toArray());
	}

	public static <T extends BasePoEntity> T apiSave(Class<T> tClazz, Map<String, Object> paramsMap, BasePoEntity target, String... properties) throws Exception {
		RestfulApiProxy apiProxy = BeanCtxUtils.getBeanByType(RestfulApiProxy.class);
		Map<String, Object> data = IcwUtils.mapObject();
		Map<String, Object> params = BeanCtxUtils.getBeanByType(DataExchange.class).parseMap(paramsMap);
		apiProxy.patch(IcwUtils.toFirstLowerCase(tClazz.getSimpleName()), params, null, null, data);
		T result = tClazz.cast(data.get("data"));
		if (target == null) {
			// skip
		} else if (properties.length == 0) {
			BeanUtils.copyProperties(result, target);
		} else {
			Object findingObject = result;
			for (String property : properties) {
				if (findingObject == null) {
					break;
				} else if (!Pattern.matches("([1-9][0-9]+)|[0-9]", property)) {
					findingObject = BeanUtils.getPropertyDescriptor(findingObject.getClass(), property).getReadMethod().invoke(findingObject);
				} else if (Collection.class.isInstance(findingObject)) {
					findingObject = ((Collection<?>) findingObject).toArray()[Integer.parseInt(property, 10)];
				} else if (findingObject.getClass().isArray()) {
					findingObject = Array.get(findingObject, Integer.parseInt(property, 10));
				} else {
					throw new Exception("The settings of apiSave properties is incorrect.");
				}
			}
			if (findingObject != null) {
				BeanUtils.copyProperties(findingObject, target);
			}
		}
		return result;
	}

	public static <T extends BasePoEntity> T apiSave(Class<T> tClazz, Map<String, Object> paramsMap) throws Exception {
		return apiSave(tClazz, paramsMap, null);
	}

	@SuppressWarnings("unchecked")
	public static <T> T copyIgnore(T t, String... ignores) throws Exception {
		Object newInstance = t.getClass().getConstructor().newInstance();
		BeanUtils.copyProperties(t, newInstance, ignores);
		return (T) BeanCtxUtils.getBeanByType(DataExchange.class).parse(newInstance, t.getClass());
	}

	public static String uuid() {
		return UUID.randomUUID().toString().replaceAll("-", "");
	}

	public static void setFileName(HttpServletResponse response, String fileName) throws Exception {
		response.setHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(fileName, "utf-8"));
	}

	public static <T> T findArgs(Object[] args, Class<T> class1) {
		for (int i = 0; i < args.length; i++) {
			if (class1.isInstance(args[i])) {
				return class1.cast(args[i]);
			}
		}
		return null;
	}

	public static Class<?> findImpl(String packagePath, Class<?> class1) throws Exception {
		Class<?> cc = ClassUtil.getClasses(packagePath).stream().filter(c -> {
			return Stream.of(c.getInterfaces()).allMatch(c0 -> c0 == class1);
		}).findFirst().get();
		return cc;
	}

	public static String classExecution(Class<?> clazz) {
		return "execution (* " + clazz.getName() + ".*(..))";
	}

	public static String cutInterfaceExpression(Class<?> infc) throws Exception {
		String packageName = infc.getPackage().getName().replace("decoupling.defi", "decoupling.impl");
		Class<?> proxy = IcwUtils.findImpl(packageName, infc);
		return IcwUtils.classExecution(proxy);
	}

	public static Pointcut cutInterface(Class<?> infc) throws Exception {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression(IcwUtils.cutInterfaceExpression(infc));
		return pointcut;
	}

	public static Advisor cutInterface(MethodInterceptor advice, Class<?> infc, int order) throws Exception {
		DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(IcwUtils.cutInterface(infc), advice);
		advisor.setOrder(order);
		return advisor;
	}

	public interface VoidMethodInterceptor {

		void invoke(org.aopalliance.intercept.MethodInvocation invocation) throws Throwable;

		default Object invokeWithReturn(org.aopalliance.intercept.MethodInvocation invocation) throws Throwable {
			this.invoke(invocation);
			return invocation.proceed();
		};
	}

	public static Advisor cutInterface(VoidMethodInterceptor voidAdvice, Class<?> infc, int order) throws Exception {
		return cutInterface(voidAdvice::invokeWithReturn, infc, order);
	}

	public static Advisor cutExpression(MethodInterceptor advice, String expression, int order) {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression(expression);
		DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
		advisor.setOrder(order);
		return advisor;
	}

	public static Advisor cutExpression(VoidMethodInterceptor voidAdvice, String expression, int order) {
		return cutExpression(voidAdvice::invokeWithReturn, expression, order);
	}

	public static void makeCorsHeader(HttpServletResponse resp) {
		resp.setHeader("content-type", "application/json;charset=utf-8");
		resp.setHeader("Access-Control-Allow-Origin", "*");
		resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
		resp.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,token");
	}

	public static List<Object> listObj() {
		return new LinkedList<>();
	}

	public static Object mgrSave(String entityType, Map<String, Object> value) throws Exception {
		Class<? extends BasePoEntity> clazz = ConstSettings.LRsrcEntityMap.get(IcwUtils.toFirstLowerCase(entityType));
		return mgrSave(clazz, value);
	}

	public static <T extends BasePoEntity> T mgrSave(Class<T> clazz, Map<String, Object> value) throws Exception {
		RestfulMgrProxy mgr = BeanCtxUtils.getBeanByType(RestfulMgrProxy.class);
		Map<String, Object> out = IcwUtils.mapObject();
		String id = (String) value.get("id");
		if (id != null && id.indexOf("tempid_") != 0) {
			mgr.patch(clazz, value, null, null, out);
		} else {
			mgr.post(clazz, value, null, null, out);
		}
		return clazz.cast(out.get("data"));
	}

	public static <A> A trySuccess(MyRunnable run, Callable<A> success, ErrorCall<A> error) {
		try {
			run.run();
			return success.call();
		} catch (Exception e) {
			return tryCatch(() -> error.call(e));
		}
	}

	public static interface ErrorCall<A> {

		A call(Throwable ex) throws Exception;
	}

	public static <A, B> B tryAndCatch(CallThrowable<A> call, TryAndCatch<A, B> tc) throws Throwable {
		A res = null;
		Throwable e = null;
		try {
			res = call.exec();
		} catch (Throwable ex) {
			e = ex;
		}
		return tc.exec(res, e);
	}

	public static interface CallThrowable<A> {

		A exec() throws Throwable;
	}

	public static interface TryAndCatch<TRES, ERES> {

		ERES exec(TRES res, Throwable e) throws Throwable;
	}

	public static <T> Map<String, T> instanceMap(Class<T> defi, Map<String, T> map) throws Exception {
		String packageName = defi.getPackage().getName().replace(".defi.", ".impl.") + "." + IcwUtils.toUnderScoreCase(defi.getSimpleName());
		Set<Class<?>> classes = ClassUtil.getClasses(packageName);
		for (Class<?> clazz : classes) {
			if (!defi.isAssignableFrom(clazz)) {
				continue;
			} else if (!Modifier.isPublic(clazz.getModifiers())) {
				continue;
			} else if (Modifier.isAbstract(clazz.getModifiers())) {
				continue;
			} else {
				T o = defi.cast(clazz.getConstructor().newInstance());
				PropertyDescriptor pd = IcwUtils.ifNull(BeanUtils.getPropertyDescriptor(clazz, "name"), () -> BeanUtils.getPropertyDescriptor(clazz, "key"));
				String keyName;
				if (pd == null || pd.getReadMethod() == null) {
					throw new Exception("No \"name\" or \"key\" property in class[" + clazz.getName() + "]");
				} else if (map.containsKey(keyName = (String) pd.getReadMethod().invoke(o))) {
					throw new Exception("Duplicate singleton key of class [" + map.get(keyName).getClass().getName() + "] and [" + o.getClass().getName() + "]");
				} else {
					map.put(keyName, o);
				}
			}
		}
		return map;
	}

	public static <K, V> V putIfAbsent(Map<K, V> map, K key, Callable<V> vcall) throws Exception {
		V v = map.get(key);
		if (v == null) {
			map.put(key, v = vcall.call());
		}
		return v;
	}

	public static Set<Class<?>> getAllEntities() throws Exception {
		Set<Class<?>> classes = ClassUtil.getClasses(ConstSettings.RESOURCE_ENTY_PACKAGE);
		classes = classes.stream().filter(c -> BasePoEntity.class.isAssignableFrom(c) && Modifier.isPublic(c.getModifiers())).collect(Collectors.toSet());
		return new LinkedHashSet<Class<?>>(classes);
	}

	@SuppressWarnings("unchecked")
	public static Map<String, Object> castMap(Object value) {
		return Map.class.cast(value);
	}

	public static void render(HttpServletResponse resp, Object obj) throws Exception {
		String res = obj == null ? null : obj instanceof String ? (String) obj : BeanCtxUtils.getBeanByType(DataExchange.class).serialize(obj);
		IcwUtils.makeCorsHeader(resp);
		if (res != null) {
//			int cntLen = res.getBytes("utf-8").length;
//			System.out.println("content-length:" + cntLen);
//			resp.setHeader("content-length", "" + cntLen);
			resp.getWriter().write(res);
		}
	}
}
